package ru.myrtle.jst.emitter.java;

import java.io.PrintWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import ru.myrtle.jst.i18n.NLS;
import ru.myrtle.jst.Problem;
import ru.myrtle.jst.Project;
import ru.myrtle.jst.ast.ConstructorParameter;
import ru.myrtle.jst.ast.Declaration;
import ru.myrtle.jst.ast.Invoke;
import ru.myrtle.jst.ast.Parameter;
import ru.myrtle.jst.ast.Text;
import ru.myrtle.jst.ast.Unit;
import ru.myrtle.jst.emitter.Emitter;
import ru.myrtle.jst.ast.Expression;
import ru.myrtle.jst.ast.Scriptlet;

public class JavaEmitter implements Emitter {

    class ImportCollector extends IncludeAwareVisitor<Void> {

        private final Set<String> imports =
                new TreeSet<String>();

        protected ImportCollector(Project compiler) {
            super(compiler);
        }

        @Override
        public boolean visit(Unit unit, Void context) {
            imports.addAll(
                    unit.getImports()
            );

            return true;
        }

        void collect(Unit unit) {
            unit.accept(this, null);
        }

        Set<String> getImports() {
            return imports;
        }
    }

    /*
     *
     */

    class InvokeCollector extends IncludeAwareVisitor<Void> {

        private final Map<String, Invoke> invokes =
                new HashMap<String, Invoke>();

        protected InvokeCollector(Project project) {
            super(project);
        }

        @Override
        public boolean visit(Invoke invoke, Void context) {
            Invoke existing = invokes.get(
                    invoke.getFile()
            );

            if (existing == null) {
                invokes.put(
                        invoke.getFile(),
                        invoke
                );
            }

            return true;
        }

        void collect(Unit unit) {
            unit.accept(this, null);
        }

        Map<String, Invoke> getInvokes() {
            return invokes;
        }
    }

    /*
     *
     */

    class DeclarationVisitor extends IncludeAwareVisitor<PrintWriter> {

        protected DeclarationVisitor(Project compiler) {
            super(compiler);
        }

        @Override
        public boolean visit(Declaration declaration, PrintWriter out) {
            out.print("    ");
            out.print(
                    declaration.getContent()
            );
            out.print("\t");
            out.println();

            return true;
        }
    }

    /*
     *
     */

    class ConstantVisitor extends IncludeAwareVisitor<PrintWriter> {

        private int textCount = 0;

        protected ConstantVisitor(Project compiler) {
            super(compiler);
        }

        @Override
        public boolean visit(Text text, PrintWriter out) {
            String textLine = text.getText();

            if (textLine.length() > 0) {
                textCount++;

                out.print("    protected final String TEXT_");
                out.print(textCount);
                out.print(" = ");
                out.print(
                        JavaUtil.asJavaQuoteStringWithNLRemoved(textLine)
                );
                out.print(";");
                out.println();

            }
            return true;
        }
    }

    /*
     *
     */

    class ContentVisitor extends IncludeAwareVisitor<PrintWriter> {

        private int textCount = 0;

        protected ContentVisitor(Project compiler) {
            super(compiler);
        }

        @Override
        public boolean visit(Text text, PrintWriter out) {
            if (text.getText().length() > 0) {
                textCount++;

                out.print("        stringBuffer.append(TEXT_");
                out.print(textCount);
                out.print(");");
                out.println();

            }
            return true;
        }

        @Override
        public boolean visit(Scriptlet scriptlet, PrintWriter out) {
            out.print(
                    scriptlet.getContent()
            );
            out.println();

            return true;
        }

        @Override
        public boolean visit(Expression expression, PrintWriter out) {
            out.print("        stringBuffer.append(");
            out.print(
                    expression.getContent()
            );
            out.print(");");
            out.println();

            return true;
        }

        @Override
        public boolean visit(Invoke invoke, PrintWriter out) {
            String methodName = toMethodName(
                    invoke.getFile()
            );

            out.print("        stringBuffer.append(");
            out.println();

            out.print("            ");
            out.print(methodName);
            out.print("(");
            out.print(
                    invoke.getParameters()
            );
            out.print(")");
            out.println();
            out.print("        );");
            out.println();

            return true;
        }
    }

    /*
     *
     */

    public void emit(Project project, Unit unit, Writer writer) {
        PrintWriter out = new PrintWriter(writer);

        try {
            out.print("package ");
            out.print(
                    unit.getOutputPackage()
            );
            out.print(";");
            out.println();
            out.println();

            ImportCollector importCollector = new ImportCollector(project);
            importCollector.collect(unit);

            for (String importName : importCollector.getImports()) {
                out.print("import ");
                out.print(importName);
                out.print(";");
                out.println();
            }

            out.println();
            out.print("public class ");
            out.print(
                    unit.getOutputClassName()
            );

            String extendsName = unit.getOutputClassExtends();

            if (extendsName != null) {
                out.print(" extends ");
                out.print(extendsName);
            }

            Set<String> implementNames = unit.getOutputClassImplements();

            if (implementNames.size() > 0) {
                out.print(" implements ");

                for (Iterator<String> it = implementNames.iterator(); it.hasNext();) {
                    String implementName = it.next();

                    out.print(implementName);

                    if (it.hasNext()) {
                        out.print(", ");
                    }
                }
            }

            out.println();
            out.print("{");
            out.println();

            unit.accept(
                    new DeclarationVisitor(project), out
            );

            // NL

            out.print("    protected final String NL = System.getProperties().getProperty(\"line.separator\");");
            out.println();

            // Constants

            unit.accept(
                    new ConstantVisitor(project), out
            );

            // Constructor

            out.println();
            out.println("    /*");
            out.println("     *");
            out.println("     */");
            out.println();

            for (Iterator<ConstructorParameter> it = unit.getConstructorParameters().iterator(); it.hasNext();) {
                ConstructorParameter parameter = it.next();

                out.print("    private final ");
                out.print(
                        parameter.getType()
                );
                out.print(" ");
                out.print(
                        parameter.getName()
                );
                out.print(";");
                out.println();
            }

            out.println();
            out.print("    public ");
            out.print(
                    unit.getOutputClassName()
            );
            out.print("(");

            for (Iterator<ConstructorParameter> it = unit.getConstructorParameters().iterator(); it.hasNext();) {
                ConstructorParameter parameter = it.next();

                out.print(
                        parameter.getType()
                );
                out.print(" ");
                out.print(
                        parameter.getName()
                );

                if (it.hasNext()) {
                    out.print(", ");
                }
            }

            out.print(") {");
            out.println();

            for (Iterator<ConstructorParameter> it = unit.getConstructorParameters().iterator(); it.hasNext();) {
                ConstructorParameter parameter = it.next();

                out.print("        this.");
                out.print(
                        parameter.getName()
                );
                out.print(" = ");
                out.print(
                        parameter.getName()
                );
                out.print(";");
                out.println();
            }

            out.println();
            out.print("    }");
            out.println();

            // Generate method

            out.println();
            out.println("    /*");
            out.println("     *");
            out.println("     */");

            out.println();
            out.print("    public String generate(");

            for (Iterator<Parameter> it = unit.getParameters().iterator(); it.hasNext();) {
                Parameter parameter = it.next();

                out.print(
                        parameter.getType()
                );
                out.print(" ");
                out.print(
                        parameter.getName()
                );

                if (it.hasNext()) {
                    out.print(", ");
                }
            }

            out.print(")");

            if (unit.getExceptions().size() > 0) {
                out.print(" throws ");
                
                for (Iterator<String> it = unit.getExceptions().iterator(); it.hasNext();) {
                    String exception = it.next();

                    out.print(exception);

                    if (it.hasNext()) {
                        out.print(", ");
                    }
                }
            }

            out.println();

            out.print("    {");
            out.println();
            out.print("        final StringBuffer stringBuffer = new StringBuffer();");
            out.println();

            unit.accept(
                    new ContentVisitor(project), out
            );

            out.println();
            out.print("        return stringBuffer.toString();");
            out.println();
            out.print("    }");
            out.println();

            out.println();

            out.print("    // ---------------------------------------------------------------------------------------");
            out.println();
            out.print("    // ---------------------------------------------------------------------------------------");
            out.println();
            out.println();

            InvokeCollector invokeCollector = new InvokeCollector(project);
            invokeCollector.collect(unit);

            for (Map.Entry<String, Invoke> entry : invokeCollector.getInvokes().entrySet()) {
                Invoke invoke = entry.getValue();

                String methodName = toMethodName(
                        invoke.getFile()
                );

                out.print("    private String ");
                out.print(methodName);
                out.print("(");

                Unit invokedUnit = project.getUnit(
                        invoke.getFile()
                );

                if (invokedUnit != null) {
                    for (Iterator<Parameter> it = invokedUnit.getParameters().iterator(); it.hasNext();) {
                        Parameter parameter = it.next();

                        out.print(
                                parameter.getType()
                        );
                        out.print(" ");
                        out.print(
                                parameter.getName()
                        );

                        if (it.hasNext()) {
                            out.print(", ");
                        }
                    }

                    out.print(")");
                    out.println();
                    out.print("    {");
                    out.println();

                    String namespace = invokedUnit.getOutputPackage();
                    String name = invokedUnit.getOutputClassName();

                    String fullName = namespace + "." + name;

                    out.print("        ");
                    out.print(fullName);
                    out.print(" instance = new ");
                    out.print(fullName);
                    out.print("();");
                    out.println();

                    out.print("        return instance.generate(");

                    for (Iterator<Parameter> it = invokedUnit.getParameters().iterator(); it.hasNext();) {
                        Parameter parameter = it.next();

                        out.print(
                                parameter.getName()
                        );

                        if (it.hasNext()) {
                            out.print(", ");
                        }
                    }

                    out.print(");");
                    out.println();

                    out.print("    }");
                    out.println();

                    out.println();
                }
                else {
                    project.addProblems(
                            new Problem(
                                    invoke.getUnit(),
                                    Problem.Severity.ERROR,
                                    String.format(
                                            NLS.BUNDLE.getString(NLS.KEY_MISSING_INCLUDE), invoke.getFile()
                                    ),
                                    invoke.getLine(),
                                    invoke.getColumn()
                            )
                    );

                }
            }

            out.print("}");
            out.println();
        }
        finally {
            out.flush();
        }
    }

    private static String toMethodName(String s) {
        return "invoke" + toCamelCase(
                normalize(s), '_'
        );
    }

    private static String normalize(String s) {
        if (s == null) {
            return null;
        }

        StringBuffer buffer = new StringBuffer();

        for (Character character : s.toCharArray()) {
            if (Character.isLetterOrDigit(character)) {
                buffer.append(character);
            }
            else {
                buffer.append('_');
            }
        }

        return buffer.toString();
    }

    private static String toCamelCase(String name, char delim) {
        if (name != null && name.length() > 0) {
            StringBuffer buffer = new StringBuffer();

            String[] parts = name.split("[" + delim + "]");

            for (String part : parts) {
                buffer.append(
                        capitalize(part.toLowerCase()
                        )
                );
            }

            return buffer.toString();
        }

        return name;
    }

    private static String capitalize(String s) {
        if (s == null) {
            return null;
        }

        if (s.length() > 0) {
            return Character.toUpperCase(
                    s.charAt(0)
            ) + (s.length() > 1 ? s.substring(1) : "");
        }
        else {
            return s;
        }
    }
}
